/*
 * NOTICE: THE FILE HAS BEEN MODIFIED TO SUIT THE NEEDS OF THE PROJECT.
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.robobinding.internal.java_beans;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;

@SuppressWarnings({ "rawtypes", "unchecked" })
class StandardBeanInfo extends SimpleBeanInfo {

	// Prefixes for methods that set or get a Property
	private static final String PREFIX_IS = "is"; //$NON-NLS-1$

	private static final String PREFIX_GET = "get"; //$NON-NLS-1$

	private static final String PREFIX_SET = "set"; //$NON-NLS-1$

	private static final String STR_NORMAL = "normal"; //$NON-NLS-1$

	private static final String STR_INDEXED = "indexed"; //$NON-NLS-1$

	private static final String STR_VALID = "valid"; //$NON-NLS-1$

	private static final String STR_INVALID = "invalid"; //$NON-NLS-1$

	private static final String STR_PROPERTY_TYPE = "PropertyType"; //$NON-NLS-1$

	private static final String STR_IS_CONSTRAINED = "isConstrained"; //$NON-NLS-1$

	private static final String STR_SETTERS = "setters"; //$NON-NLS-1$

	private static final String STR_GETTERS = "getters"; //$NON-NLS-1$

	private boolean explicitProperties = false;

	private BeanInfo explicitBeanInfo = null;

	private MethodDescriptor[] methods = null;

	private PropertyDescriptor[] properties = null;

	private BeanDescriptor beanDescriptor = null;

	BeanInfo[] additionalBeanInfo = null;

	private Class<?> beanClass;

	private int defaultPropertyIndex = -1;

	private static PropertyComparator comparator = new PropertyComparator();

	private boolean canAddPropertyChangeListener;

	private boolean canRemovePropertyChangeListener;

	StandardBeanInfo(Class<?> beanClass, BeanInfo explicitBeanInfo, Class<?> stopClass) throws IntrospectionException {
		this.beanClass = beanClass;
		/*--------------------------------------------------------------------------------------
		 * There are 3 aspects of BeanInfo that must be supplied:
		 * a) PropertyDescriptors
		 * b) MethodDescriptors
		 * c) EventSetDescriptors
		 * Each of these may be optionally provided in the explicitBeanInfo object relating to
		 * this bean.  Where the explicitBeanInfo provides one of these aspects, it is used
		 * without question and no introspection of the beanClass is performed for that aspect.
		 * There are also 3 optional items of BeanInfo that may be provided by the
		 * explicitBeanInfo object:
		 * 1) BeanDescriptor
		 * 2) DefaultEventIndex
		 * 3) DefaultPropertyIndex
		 * These aspects of the beanClass cannot be derived through introspection of the class.
		 * If they are not provided by the explicitBeanInfo, then they must be left null in the
		 * returned BeanInfo, otherwise they will be copied from the explicitBeanInfo
		 --------------------------------------------------------------------------------------*/
		if (explicitBeanInfo != null) {
			this.explicitBeanInfo = explicitBeanInfo;
			properties = explicitBeanInfo.getPropertyDescriptors();
			additionalBeanInfo = explicitBeanInfo.getAdditionalBeanInfo();
			if (properties != null)
				explicitProperties = true;
		}

		if (methods == null) {
			methods = introspectMethods();
		}

		if (properties == null) {
			properties = introspectProperties(stopClass);
		}
	}

	@Override
	public BeanInfo[] getAdditionalBeanInfo() {
		return null;
	}

	@Override
	public PropertyDescriptor[] getPropertyDescriptors() {
		return properties;
	}

	@Override
	public BeanDescriptor getBeanDescriptor() {
		if (beanDescriptor == null) {
			if (explicitBeanInfo != null) {
				beanDescriptor = explicitBeanInfo.getBeanDescriptor();
			}
			if (beanDescriptor == null) {
				beanDescriptor = new BeanDescriptor(beanClass);
			}
		}
		return beanDescriptor;
	}

	void mergeBeanInfo(BeanInfo beanInfo, boolean force) throws IntrospectionException {
		if (force || !explicitProperties) {
			PropertyDescriptor[] superDescs = beanInfo.getPropertyDescriptors();
			if (superDescs != null) {
				if (getPropertyDescriptors() != null) {
					properties = mergeProps(superDescs, beanInfo.getDefaultPropertyIndex());
				} else {
					properties = superDescs;
					defaultPropertyIndex = beanInfo.getDefaultPropertyIndex();
				}
			}
		}
	}

	/*
	 * merge the PropertyDescriptor with superclass
	 */
	private PropertyDescriptor[] mergeProps(PropertyDescriptor[] superDescs, int superDefaultIndex) throws IntrospectionException {
		// change to OO way as EventSetD and MethodD
		HashMap<String, PropertyDescriptor> subMap = internalAsMap(properties);
		String defaultPropertyName = null;
		if (defaultPropertyIndex >= 0 && defaultPropertyIndex < properties.length) {
			defaultPropertyName = properties[defaultPropertyIndex].getName();
		} else if (superDefaultIndex >= 0 && superDefaultIndex < superDescs.length) {
			defaultPropertyName = superDescs[superDefaultIndex].getName();
		}

		for (int i = 0; i < superDescs.length; i++) {
			PropertyDescriptor superDesc = superDescs[i];
			String propertyName = superDesc.getName();
			if (!subMap.containsKey(propertyName)) {
				subMap.put(propertyName, superDesc);
				continue;
			}

			Object value = subMap.get(propertyName);
			// if sub and super are both PropertyDescriptor
			Method subGet = ((PropertyDescriptor) value).getReadMethod();
			Method subSet = ((PropertyDescriptor) value).getWriteMethod();
			Method superGet = superDesc.getReadMethod();
			Method superSet = superDesc.getWriteMethod();

			Class<?> superType = superDesc.getPropertyType();
			Class<?> superIndexedType = null;
			Class<?> subType = ((PropertyDescriptor) value).getPropertyType();
			Class<?> subIndexedType = null;

			if (value instanceof IndexedPropertyDescriptor) {
				subIndexedType = ((IndexedPropertyDescriptor) value).getIndexedPropertyType();
			}
			if (superDesc instanceof IndexedPropertyDescriptor) {
				superIndexedType = ((IndexedPropertyDescriptor) superDesc).getIndexedPropertyType();
			}

			// if superDesc is PropertyDescriptor
			if (superIndexedType == null) {
				PropertyDescriptor subDesc = (PropertyDescriptor) value;
				// Sub is PropertyDescriptor
				if (subIndexedType == null) {
					// Same property type
					if (subType != null && superType != null && subType.getName() != null && subType.getName().equals(superType.getName())) {
						if (superGet != null && (subGet == null || superGet.equals(subGet))) {
							subDesc.setReadMethod(superGet);
						}
						if (superSet != null && (subSet == null || superSet.equals(subSet))) {
							subDesc.setWriteMethod(superSet);
						}
						if (subType == boolean.class && subGet != null && superGet != null) {
							if (superGet.getName().startsWith(PREFIX_IS)) {
								subDesc.setReadMethod(superGet);
							}
						}
					} else { // Different type
						if ((subGet == null || subSet == null) && (superGet != null)) {
							subDesc = new PropertyDescriptor(propertyName, superGet, superSet);
							if (subGet != null) {
								String subGetName = subGet.getName();
								Method method = null;
								MethodDescriptor[] introspectMethods = introspectMethods();
								for (MethodDescriptor methodDesc : introspectMethods) {
									method = methodDesc.getMethod();
									if (method != subGet && subGetName.equals(method.getName()) && method.getParameterTypes().length == 0
											&& method.getReturnType() == superType) {
										subDesc.setReadMethod(method);
										break;
									}
								}
							}
						}
					}
				} else { // Sub is IndexedPropertyDescriptor and super is
					// PropertyDescriptor
					if (superType != null && (superType.isArray()) && (superType.getComponentType().getName().equals(subIndexedType.getName()))) {
						if ((subGet == null) && (superGet != null)) {
							subDesc.setReadMethod(superGet);
						}
						if ((subSet == null) && (superSet != null)) {
							subDesc.setWriteMethod(superSet);
						}
					} // different type do nothing
						// sub is indexed pd and super is normal pd
					if (subIndexedType == boolean.class && superType == boolean.class) {
						Method subIndexedSet = ((IndexedPropertyDescriptor) subDesc).getIndexedWriteMethod();
						if (subGet == null && subSet == null && subIndexedSet != null && superGet != null) {
							try {
								subSet = beanClass.getDeclaredMethod(subIndexedSet.getName(), boolean.class);
							} catch (Exception e) {
								// ignored
							}
							if (subSet != null) {
								// Cast sub into PropertyDescriptor
								subDesc = new PropertyDescriptor(propertyName, superGet, subSet);
							}
						}
					}
				}
				subMap.put(propertyName, subDesc);
			} else { // Super is IndexedPropertyDescriptor
				if (subIndexedType == null) { // Sub is PropertyDescriptor
					if (subType != null && subType.isArray() && (subType.getComponentType().getName().equals(superIndexedType.getName()))) {
						// Same type
						if (subGet != null) {
							superDesc.setReadMethod(subGet);
						}
						if (subSet != null) {
							superDesc.setWriteMethod(subSet);
						}
						subMap.put(propertyName, superDesc);
					} else {
						// subDesc is PropertyDescriptor
						// superDesc is IndexedPropertyDescriptor

						// fill null subGet or subSet method with superClass's
						if (subGet == null || subSet == null) {
							Class<?> beanSuperClass = beanClass.getSuperclass();
							String methodSuffix = capitalize(propertyName);
							Method method = null;
							if (subGet == null) {
								// subGet is null
								if (subType == boolean.class) {
									try {
										method = beanSuperClass.getDeclaredMethod(PREFIX_IS + methodSuffix);
									} catch (Exception e) {
										// ignored
									}
								} else {
									try {
										method = beanSuperClass.getDeclaredMethod(PREFIX_GET + methodSuffix);
									} catch (Exception e) {
										// ignored
									}
								}
								if (method != null && !Modifier.isStatic(method.getModifiers()) && method.getReturnType() == subType) {
									((PropertyDescriptor) value).setReadMethod(method);
								}
							} else {
								// subSet is null
								try {
									method = beanSuperClass.getDeclaredMethod(PREFIX_SET + methodSuffix, subType);
								} catch (Exception e) {
									// ignored
								}
								if (method != null && !Modifier.isStatic(method.getModifiers()) && method.getReturnType() == void.class) {
									((PropertyDescriptor) value).setWriteMethod(method);
								}
							}
						}
						subMap.put(propertyName, (PropertyDescriptor) value);
					}
				} else if (subIndexedType.getName().equals(superIndexedType.getName())) {
					// Sub is IndexedPropertyDescriptor and Same type
					IndexedPropertyDescriptor subDesc = (IndexedPropertyDescriptor) value;
					if ((subGet == null) && (superGet != null)) {
						subDesc.setReadMethod(superGet);
					}
					if ((subSet == null) && (superSet != null)) {
						subDesc.setWriteMethod(superSet);
					}
					IndexedPropertyDescriptor superIndexedDesc = (IndexedPropertyDescriptor) superDesc;

					if ((subDesc.getIndexedReadMethod() == null) && (superIndexedDesc.getIndexedReadMethod() != null)) {
						subDesc.setIndexedReadMethod(superIndexedDesc.getIndexedReadMethod());
					}

					if ((subDesc.getIndexedWriteMethod() == null) && (superIndexedDesc.getIndexedWriteMethod() != null)) {
						subDesc.setIndexedWriteMethod(superIndexedDesc.getIndexedWriteMethod());
					}

					subMap.put(propertyName, subDesc);
				} // Different indexed type, do nothing
			}
			mergeAttributes((PropertyDescriptor) value, superDesc);
		}

		PropertyDescriptor[] theDescs = new PropertyDescriptor[subMap.size()];
		subMap.values().toArray(theDescs);

		if (defaultPropertyName != null && !explicitProperties) {
			for (int i = 0; i < theDescs.length; i++) {
				if (defaultPropertyName.equals(theDescs[i].getName())) {
					defaultPropertyIndex = i;
					break;
				}
			}
		}
		return theDescs;
	}

	private String capitalize(String name) {
		if (name == null) {
			return null;
		}
		// The rule for decapitalize is that:
		// If the first letter of the string is Upper Case, make it lower case
		// UNLESS the second letter of the string is also Upper Case, in which
		// case no
		// changes are made.
		if (name.length() == 0 || (name.length() > 1 && Character.isUpperCase(name.charAt(1)))) {
			return name;
		}

		char[] chars = name.toCharArray();
		chars[0] = Character.toUpperCase(chars[0]);
		return new String(chars);
	}

	private static void mergeAttributes(PropertyDescriptor subDesc, PropertyDescriptor superDesc) {
		// this is just temp workaround, need more elegant solution to
		// handle this
		subDesc.hidden |= superDesc.hidden;
		subDesc.expert |= superDesc.expert;
		subDesc.preferred |= superDesc.preferred;
		subDesc.bound |= superDesc.bound;
		subDesc.constrained |= superDesc.constrained;
		subDesc.name = superDesc.name;
		if (subDesc.shortDescription == null && superDesc.shortDescription != null) {
			subDesc.shortDescription = superDesc.shortDescription;
		}
		if (subDesc.displayName == null && superDesc.displayName != null) {
			subDesc.displayName = superDesc.displayName;
		}
	}

	private static HashMap<String, PropertyDescriptor> internalAsMap(PropertyDescriptor[] propertyDescs) {
		HashMap<String, PropertyDescriptor> map = new HashMap<String, PropertyDescriptor>();
		for (int i = 0; i < propertyDescs.length; i++) {
			map.put(propertyDescs[i].getName(), propertyDescs[i]);
		}
		return map;
	}

	/**
	 * Introspects the supplied class and returns a list of the public methods
	 * of the class.
	 * 
	 * @return An array of MethodDescriptors with the public methods. null if
	 *         there are no public methods
	 */
	private MethodDescriptor[] introspectMethods() {
		return introspectMethods(false, beanClass);
	}

	private MethodDescriptor[] introspectMethods(boolean includeSuper) {
		return introspectMethods(includeSuper, beanClass);
	}

	private MethodDescriptor[] introspectMethods(boolean includeSuper, Class<?> introspectorClass) {

		// Get the list of methods belonging to this class
		Method[] basicMethods = includeSuper ? introspectorClass.getMethods() : introspectorClass.getDeclaredMethods();

		if (basicMethods == null || basicMethods.length == 0)
			return null;

		ArrayList<MethodDescriptor> methodList = new ArrayList<MethodDescriptor>(basicMethods.length);

		// Loop over the methods found, looking for public non-static methods
		for (int i = 0; i < basicMethods.length; i++) {
			int modifiers = basicMethods[i].getModifiers();
			if (Modifier.isPublic(modifiers)) {
				// Allocate a MethodDescriptor for this method
				MethodDescriptor theDescriptor = new MethodDescriptor(basicMethods[i]);
				methodList.add(theDescriptor);
			}
		}

		// Get the list of public methods into the returned array
		int methodCount = methodList.size();
		MethodDescriptor[] theMethods = null;
		if (methodCount > 0) {
			theMethods = new MethodDescriptor[methodCount];
			theMethods = methodList.toArray(theMethods);
		}

		return theMethods;
	}

	/**
	 * Introspects the supplied class and returns a list of the Properties of
	 * the class.
	 * 
	 * @param stopClass
	 *            - the to introspecting at
	 * @return The list of Properties as an array of PropertyDescriptors
	 * @throws IntrospectionException
	 */
	private PropertyDescriptor[] introspectProperties(Class<?> stopClass) throws IntrospectionException {

		// Get descriptors for the public methods
		MethodDescriptor[] methodDescriptors = introspectMethods();

		if (methodDescriptors == null) {
			return null;
		}

		ArrayList<MethodDescriptor> methodList = new ArrayList<MethodDescriptor>();
		// Loop over the methods found, looking for public non-static methods
		for (int index = 0; index < methodDescriptors.length; index++) {
			int modifiers = methodDescriptors[index].getMethod().getModifiers();
			if (!Modifier.isStatic(modifiers)) {
				methodList.add(methodDescriptors[index]);
			}
		}

		// Get the list of public non-static methods into an array
		int methodCount = methodList.size();
		MethodDescriptor[] theMethods = null;
		if (methodCount > 0) {
			theMethods = new MethodDescriptor[methodCount];
			theMethods = methodList.toArray(theMethods);
		}

		if (theMethods == null) {
			return null;
		}

		HashMap<String, HashMap> propertyTable = new HashMap<String, HashMap>(theMethods.length);

		// Search for methods that either get or set a Property
		for (int i = 0; i < theMethods.length; i++) {
			introspectGet(theMethods[i].getMethod(), propertyTable);
			introspectSet(theMethods[i].getMethod(), propertyTable);
		}

		// fix possible getter & setter collisions
		fixGetSet(propertyTable);

		// If there are listener methods, should be bound.
		MethodDescriptor[] allMethods = introspectMethods(true);
		if (stopClass != null) {
			MethodDescriptor[] excludeMethods = introspectMethods(true, stopClass);
			if (excludeMethods != null) {
				ArrayList<MethodDescriptor> tempMethods = new ArrayList<MethodDescriptor>();
				for (MethodDescriptor method : allMethods) {
					if (!isInSuper(method, excludeMethods)) {
						tempMethods.add(method);
					}
				}
				allMethods = tempMethods.toArray(new MethodDescriptor[0]);
			}
		}
		// Put the properties found into the PropertyDescriptor array
		ArrayList<PropertyDescriptor> propertyList = new ArrayList<PropertyDescriptor>();

		for (Map.Entry<String, HashMap> entry : propertyTable.entrySet()) {
			String propertyName = entry.getKey();
			HashMap table = entry.getValue();
			if (table == null) {
				continue;
			}
			String normalTag = (String) table.get(STR_NORMAL);
			String indexedTag = (String) table.get(STR_INDEXED);

			if ((normalTag == null) && (indexedTag == null)) {
				continue;
			}

			Method get = (Method) table.get(STR_NORMAL + PREFIX_GET);
			Method set = (Method) table.get(STR_NORMAL + PREFIX_SET);
			Method indexedGet = (Method) table.get(STR_INDEXED + PREFIX_GET);
			Method indexedSet = (Method) table.get(STR_INDEXED + PREFIX_SET);

			PropertyDescriptor propertyDesc = null;
			if (indexedTag == null) {
				propertyDesc = new PropertyDescriptor(propertyName, get, set);
			} else {
				try {
					propertyDesc = new IndexedPropertyDescriptor(propertyName, get, set, indexedGet, indexedSet);
				} catch (IntrospectionException e) {
					// If the getter and the indexGetter is not compatible, try
					// getter/setter is null;
					propertyDesc = new IndexedPropertyDescriptor(propertyName, null, null, indexedGet, indexedSet);
				}
			}
			// RI set propretyDescriptor as bound.
			// propertyDesc.setBound(true);
			if (canAddPropertyChangeListener && canRemovePropertyChangeListener) {
				propertyDesc.setBound(true);
			} else {
				propertyDesc.setBound(false);
			}
			if (table.get(STR_IS_CONSTRAINED) == Boolean.TRUE) { //$NON-NLS-1$
				propertyDesc.setConstrained(true);
			}
			propertyList.add(propertyDesc);
		}

		PropertyDescriptor[] theProperties = new PropertyDescriptor[propertyList.size()];
		propertyList.toArray(theProperties);
		return theProperties;
	}

	private boolean isInSuper(MethodDescriptor method, MethodDescriptor[] excludeMethods) {
		for (MethodDescriptor m : excludeMethods) {
			if (method.getMethod().equals(m.getMethod())) {
				return true;
			}
		}
		return false;
	}

	private static void introspectGet(Method theMethod, HashMap<String, HashMap> propertyTable) {

		String methodName = theMethod.getName();
		int prefixLength = 0;
		String propertyName;
		Class propertyType;
		Class[] paramTypes;
		HashMap table;
		ArrayList<Method> getters;

		if (methodName == null) {
			return;
		}

		if (methodName.startsWith(PREFIX_GET)) {
			prefixLength = PREFIX_GET.length();
		}

		if (methodName.startsWith(PREFIX_IS)) {
			prefixLength = PREFIX_IS.length();
		}

		if (prefixLength == 0) {
			return;
		}

		propertyName = Introspector.decapitalize(methodName.substring(prefixLength));

		// validate property name
		if (!isValidProperty(propertyName)) {
			return;
		}

		// validate return type
		propertyType = theMethod.getReturnType();

		if (propertyType == null || propertyType == void.class) {
			return;
		}

		// isXXX return boolean
		if (prefixLength == 2) {
			if (!(propertyType == boolean.class)) {
				return;
			}
		}

		// validate parameter types
		paramTypes = theMethod.getParameterTypes();
		if (paramTypes.length > 1 || (paramTypes.length == 1 && paramTypes[0] != int.class)) {
			return;
		}

		table = propertyTable.get(propertyName);
		if (table == null) {
			table = new HashMap();
			propertyTable.put(propertyName, table);
		}

		getters = (ArrayList<Method>) table.get(STR_GETTERS);
		if (getters == null) {
			getters = new ArrayList<Method>();
			table.put(STR_GETTERS, getters);
		}

		// add current method as a valid getter
		getters.add(theMethod);
	}

	private static void introspectSet(Method theMethod, HashMap<String, HashMap> propertyTable) {

		String methodName = theMethod.getName();
		if (methodName == null) {
			return;
		}
		String propertyName;
		Class[] paramTypes;

		// setter method should never return type other than void
		/**
		 * Comment out by Cheng
		 */
		/*
		Class returnType;
		returnType = theMethod.getReturnType();
		if (returnType != void.class) {
			return;
		}*/

		if (methodName == null || !methodName.startsWith(PREFIX_SET)) {
			return;
		}

		propertyName = Introspector.decapitalize(methodName.substring(PREFIX_SET.length()));

		// validate property name
		if (!isValidProperty(propertyName)) {
			return;
		}

		// It seems we do not need to validate return type

		// validate param types
		paramTypes = theMethod.getParameterTypes();

		if (paramTypes.length == 0 || paramTypes.length > 2 || (paramTypes.length == 2 && paramTypes[0] != int.class)) {
			return;
		}

		HashMap table = propertyTable.get(propertyName);
		if (table == null) {
			table = new HashMap();
			propertyTable.put(propertyName, table);
		}

		ArrayList<Method> setters = (ArrayList<Method>) table.get(STR_SETTERS);
		if (setters == null) {
			setters = new ArrayList<Method>();
			table.put(STR_SETTERS, setters);
		}

		// handle constrained
		Class[] exceptions = theMethod.getExceptionTypes();
		for (Class e : exceptions) {
			if (e.equals(PropertyVetoException.class)) {
				table.put(STR_IS_CONSTRAINED, Boolean.TRUE); //$NON-NLS-1$
			}
		}

		// add new setter
		setters.add(theMethod);
	}

	/**
	 * Checks and fixs all cases when several incompatible checkers / getters
	 * were specified for single property.
	 * 
	 * @param propertyTable
	 * @throws IntrospectionException
	 */
	private void fixGetSet(HashMap<String, HashMap> propertyTable) throws IntrospectionException {

		if (propertyTable == null) {
			return;
		}

		for (Map.Entry<String, HashMap> entry : propertyTable.entrySet()) {
			HashMap<String, Object> table = entry.getValue();
			ArrayList<Method> getters = (ArrayList<Method>) table.get(STR_GETTERS);
			ArrayList<Method> setters = (ArrayList<Method>) table.get(STR_SETTERS);

			Method normalGetter = null;
			Method indexedGetter = null;
			Method normalSetter = null;
			Method indexedSetter = null;

			Class<?> normalPropType = null;
			Class<?> indexedPropType = null;

			if (getters == null) {
				getters = new ArrayList<Method>();
			}

			if (setters == null) {
				setters = new ArrayList<Method>();
			}

			// retrieve getters
			Class<?>[] paramTypes = null;
			String methodName = null;
			for (Method getter : getters) {
				paramTypes = getter.getParameterTypes();
				methodName = getter.getName();
				// checks if it's a normal getter
				if (paramTypes == null || paramTypes.length == 0) {
					// normal getter found
					if (normalGetter == null || methodName.startsWith(PREFIX_IS)) {
						normalGetter = getter;
					}
				}

				// checks if it's an indexed getter
				if (paramTypes != null && paramTypes.length == 1 && paramTypes[0] == int.class) {
					// indexed getter found
					if (indexedGetter == null || methodName.startsWith(PREFIX_GET)
							|| (methodName.startsWith(PREFIX_IS) && !indexedGetter.getName().startsWith(PREFIX_GET))) {
						indexedGetter = getter;
					}
				}
			}

			// retrieve normal setter
			if (normalGetter != null) {
				// Now we will try to look for normal setter of the same type.
				Class<?> propertyType = normalGetter.getReturnType();

				for (Method setter : setters) {
					if (setter.getParameterTypes().length == 1 && propertyType.equals(setter.getParameterTypes()[0])) {
						normalSetter = setter;
						break;
					}
				}
			} else {
				// Normal getter wasn't defined. Let's look for the last
				// defined setter

				for (Method setter : setters) {
					if (setter.getParameterTypes().length == 1) {
						normalSetter = setter;
					}
				}
			}

			// retrieve indexed setter
			if (indexedGetter != null) {
				// Now we will try to look for indexed setter of the same type.
				Class<?> propertyType = indexedGetter.getReturnType();

				for (Method setter : setters) {
					if (setter.getParameterTypes().length == 2 && setter.getParameterTypes()[0] == int.class
							&& propertyType.equals(setter.getParameterTypes()[1])) {
						indexedSetter = setter;
						break;
					}
				}
			} else {
				// Indexed getter wasn't defined. Let's look for the last
				// defined indexed setter

				for (Method setter : setters) {
					if (setter.getParameterTypes().length == 2 && setter.getParameterTypes()[0] == int.class) {
						indexedSetter = setter;
					}
				}
			}

			// determine property type
			if (normalGetter != null) {
				normalPropType = normalGetter.getReturnType();
			} else if (normalSetter != null) {
				normalPropType = normalSetter.getParameterTypes()[0];
			}

			// determine indexed getter/setter type
			if (indexedGetter != null) {
				indexedPropType = indexedGetter.getReturnType();
			} else if (indexedSetter != null) {
				indexedPropType = indexedSetter.getParameterTypes()[1];
			}

			// convert array-typed normal getters to indexed getters
			if (normalGetter != null && normalGetter.getReturnType().isArray()) {

			}

			// RULES
			// These rules were created after performing extensive black-box
			// testing of RI

			// RULE1
			// Both normal getter and setter of the same type were defined;
			// no indexed getter/setter *PAIR* of the other type defined
			if (normalGetter != null && normalSetter != null && (indexedGetter == null || indexedSetter == null)) {
				table.put(STR_NORMAL, STR_VALID);
				table.put(STR_NORMAL + PREFIX_GET, normalGetter);
				table.put(STR_NORMAL + PREFIX_SET, normalSetter);
				table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);
				continue;
			}

			// RULE2
			// normal getter and/or setter was defined; no indexed
			// getters & setters defined
			if ((normalGetter != null || normalSetter != null) && indexedGetter == null && indexedSetter == null) {
				table.put(STR_NORMAL, STR_VALID);
				table.put(STR_NORMAL + PREFIX_GET, normalGetter);
				table.put(STR_NORMAL + PREFIX_SET, normalSetter);
				table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);
				continue;
			}

			// RULE3
			// mix of normal / indexed getters and setters are defined. Types
			// are compatible
			if ((normalGetter != null || normalSetter != null) && (indexedGetter != null || indexedSetter != null)) {
				// (1)!A!B!C!D
				if (normalGetter != null && normalSetter != null && indexedGetter != null && indexedSetter != null) {
					if (indexedGetter.getName().startsWith(PREFIX_GET)) {
						table.put(STR_NORMAL, STR_VALID);
						table.put(STR_NORMAL + PREFIX_GET, normalGetter);
						table.put(STR_NORMAL + PREFIX_SET, normalSetter);
						table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);

						table.put(STR_INDEXED, STR_VALID);
						table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
						table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
						table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
					} else {
						if (normalPropType != boolean.class && normalGetter.getName().startsWith(PREFIX_IS)) {
							table.put(STR_INDEXED, STR_VALID);
							table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
							table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
						} else {
							table.put(STR_NORMAL, STR_VALID);
							table.put(STR_NORMAL + PREFIX_GET, normalGetter);
							table.put(STR_NORMAL + PREFIX_SET, normalSetter);
							table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);
						}
					}
					continue;
				}

				// (2)!AB!C!D
				if (normalGetter != null && normalSetter == null && indexedGetter != null && indexedSetter != null) {
					table.put(STR_NORMAL, STR_VALID);
					table.put(STR_NORMAL + PREFIX_GET, normalGetter);
					table.put(STR_NORMAL + PREFIX_SET, normalSetter);
					table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);

					table.put(STR_INDEXED, STR_VALID);
					if (indexedGetter.getName().startsWith(PREFIX_GET)) {
						table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
					}
					table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
					table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
					continue;
				}

				// (3)A!B!C!D
				if (normalGetter == null && normalSetter != null && indexedGetter != null && indexedSetter != null) {
					table.put(STR_INDEXED, STR_VALID);
					if (indexedGetter.getName().startsWith(PREFIX_GET)) {
						table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
					}
					table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
					table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
					continue;
				}

				// (4)!AB!CD
				if (normalGetter != null && normalSetter == null && indexedGetter != null && indexedSetter == null) {
					if (indexedGetter.getName().startsWith(PREFIX_GET)) {
						table.put(STR_NORMAL, STR_VALID);
						table.put(STR_NORMAL + PREFIX_GET, normalGetter);
						table.put(STR_NORMAL + PREFIX_SET, normalSetter);
						table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);

						table.put(STR_INDEXED, STR_VALID);
						table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
						table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
						table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
					} else {
						table.put(STR_NORMAL, STR_VALID);
						table.put(STR_NORMAL + PREFIX_GET, normalGetter);
						table.put(STR_NORMAL + PREFIX_SET, normalSetter);
						table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);
					}
					continue;
				}

				// (5)A!B!CD
				if (normalGetter == null && normalSetter != null && indexedGetter != null && indexedSetter == null) {
					if (indexedGetter.getName().startsWith(PREFIX_GET)) {
						table.put(STR_NORMAL, STR_VALID);
						table.put(STR_NORMAL + PREFIX_GET, normalGetter);
						table.put(STR_NORMAL + PREFIX_SET, normalSetter);
						table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);

						table.put(STR_INDEXED, STR_VALID);
						table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
						table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
						table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
					} else {
						table.put(STR_NORMAL, STR_VALID);
						table.put(STR_NORMAL + PREFIX_GET, normalGetter);
						table.put(STR_NORMAL + PREFIX_SET, normalSetter);
						table.put(STR_NORMAL + STR_PROPERTY_TYPE, normalPropType);
					}
					continue;
				}

				// (6)!ABC!D
				if (normalGetter != null && normalSetter == null && indexedGetter == null && indexedSetter != null) {
					table.put(STR_INDEXED, STR_VALID);
					table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
					table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
					table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
					continue;
				}

				// (7)A!BC!D
				if (normalGetter == null && normalSetter != null && indexedGetter == null && indexedSetter != null) {
					table.put(STR_INDEXED, STR_VALID);
					table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
					table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
					table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
					continue;
				}
			}

			// RULE4
			// no normal normal getter / setter.
			// Only indexed getter and/or setter is given
			// no normal setters / getters defined
			if (normalSetter == null && normalGetter == null && (indexedGetter != null || indexedSetter != null)) {
				if (indexedGetter != null && indexedGetter.getName().startsWith(PREFIX_IS)) {
					if (indexedSetter != null) {
						table.put(STR_INDEXED, STR_VALID);
						table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
						table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
					}
					continue;
				}
				table.put(STR_INDEXED, STR_VALID);
				table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
				table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
				table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
				continue;
			}

			// RULE5
			// Both indexed getter and setter methods are defined
			// no normal getter/setter *PAIR* of the other type defined
			if ((normalSetter != null || normalGetter != null) && indexedGetter != null && indexedSetter != null) {
				table.put(STR_INDEXED, STR_VALID);
				table.put(STR_INDEXED + PREFIX_GET, indexedGetter);
				table.put(STR_INDEXED + PREFIX_SET, indexedSetter);
				table.put(STR_INDEXED + STR_PROPERTY_TYPE, indexedPropType);
				continue;
			}

			// default rule - invalid property
			table.put(STR_NORMAL, STR_INVALID);
			table.put(STR_INDEXED, STR_INVALID);
		}

	}

	private static boolean isValidProperty(String propertyName) {
		return (propertyName != null) && (propertyName.length() != 0);
	}

	private static class PropertyComparator implements Comparator<PropertyDescriptor> {
		public int compare(PropertyDescriptor object1, PropertyDescriptor object2) {
			return object1.getName().compareTo(object2.getName());
		}

	}

	void init() {
		if (this.properties == null) {
			this.properties = new PropertyDescriptor[0];
		}

		if (properties != null) {
			String defaultPropertyName = (defaultPropertyIndex != -1 ? properties[defaultPropertyIndex].getName() : null);
			Arrays.sort(properties, comparator);
			if (null != defaultPropertyName) {
				for (int i = 0; i < properties.length; i++) {
					if (defaultPropertyName.equals(properties[i].getName())) {
						defaultPropertyIndex = i;
						break;
					}
				}
			}
		}
	}
}
